/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.radio.lastfm.service;

import ch.rolandschaer.ascrblr.radio.MetaData;
import ch.rolandschaer.ascrblr.radio.RadioService;
import ch.rolandschaer.ascrblr.radio.RadioService.Command;
import ch.rolandschaer.ascrblr.radio.StationUrl;
import ch.rolandschaer.ascrblr.util.ServiceException;
import java.awt.Component;
import java.awt.Cursor;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import javazoom.jl.player.Player;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;

/**
 *
 * @author Lukas Vlcek
 */
public class PlayerService implements Lookup.Provider {

    private static final Logger logger = Logger.getLogger(PlayerService.class.getName());
    
    public enum SearchBy { globaltags, artist };

    private static Lookup lookup;
    private static InstanceContent dynamicContent = new InstanceContent();
    
    private static SwingWorker playerSwingWorker = null;
    private static JLPlayerWrapper jlPlayer = null;
    
    private static RadioService radioService = null;
    private static StationUrl stationUrl = null;
    private static MetaData metaData = null;
    
    private static SwingWorker updaterSwingWorker = null;
    // 60 seconds (by default) before metadata change is checked
    private static final Integer DEFAUL_INTERVAL = 60;
    // then repeat the check every 10 seconds
    private static final Integer REFRESH_INTERVAL = 10;
    
    // initialization of lookup
    static {
        if (lookup == null) {
            logger.log(Level.CONFIG, "Lookup is being created...");
            lookup = new AbstractLookup(dynamicContent);
            logger.log(Level.CONFIG, "Lookup created: " + lookup);
        }
        jlPlayer = new JLPlayerWrapper();
    }
    
    private static RadioService getRadio() {
        if (radioService == null) {
            radioService = new RadioService();
        }
        return radioService;
    }
    
    public static boolean signIn(final String userName, final String userPassword, Component component) throws IOException, ServiceException {         
        try {
            if (component != null) {
                component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
            }
            getRadio().setCredentials(userName, userPassword);
        } catch (Exception e) {
            stop(component);
            if (radioService != null) { radioService = null; }
            Exceptions.printStackTrace(e);
            return false;
        } finally {
            if (component != null) {
                component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
            }
        }
        return true;
    }
    
    public static StationUrl getNewStationUrl(String type, String value) {
        value = value.replaceAll("\\s+", "%2520");
        StationUrl station = new StationUrl("lastfm://" + type + "/" + value);
        return station;
    }
    
    public static void play(final Component component) {

        if (jlPlayer.getJlPlayer() == null && playerSwingWorker == null && stationUrl != null) {
            
            playerSwingWorker = new SwingWorker() {

                @Override
                protected Object doInBackground() throws Exception {
                    if (component != null) {
                        component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                    }
                    getRadio().tuneInStation(stationUrl);
                    BufferedInputStream mp3Stream = getRadio().getMp3Stream();
                    
                    int buff = 0;
                    // 8000 is empirical magic constant
                    // I will have to find a better way how to learn
                    // if the Radio buffered enough data for the playback to start and
                    // for the metadata to be loaded. Unfortunately, the Radio library
                    // does not seem to provide anything useful now...
                    while (mp3Stream.available() < 8000) {
                        if (logger.isLoggable(Level.INFO) && buff < mp3Stream.available()) {
                            buff = mp3Stream.available();
                            logger.log(Level.INFO, "Buffering... " + buff);
                        }
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            return null;
                        }
                    }   
                    
                    MetaData md = getRadio().getMetaData();
                    Integer interval = getTrackDuration(md);
                    updateMetadata(md);
                    
                    if (component != null) {
                        component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                    }
                    
                    
                    jlPlayer.setJlPlayer(new Player(mp3Stream));
                     
                    scheduleMetadataUpdater(interval);
                    
                    jlPlayer.getJlPlayer().play();
                    return null;
                }
                
            };
            
            playerSwingWorker.execute();

        }
    }

    public static void stop(Component component) {
        
        if (updaterSwingWorker != null) {
            updaterSwingWorker.cancel(true);
            logger.log(Level.INFO, "updater is null.");
            updaterSwingWorker = null;
        }
        
        if (playerSwingWorker != null && jlPlayer.getJlPlayer() != null) {
                        
            jlPlayer.getJlPlayer().close();
            jlPlayer.setJlPlayer(null);
            updateMetadata(null);
            
            // Worker thread should be done after player is closed
            // If it is not then give it next chance...
            if (!playerSwingWorker.isDone()) {
                playerSwingWorker.cancel(true);
            }
                       
            playerSwingWorker = null;
            logger.log(Level.FINE, "Radio streaming stopped.");
            
        } else if (playerSwingWorker != null && jlPlayer.getJlPlayer() == null) {
            
            logger.log(Level.FINE, "Radio probably hanged in buffering...");
            try {
                updateMetadata(getRadio().getMetaData());
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            } finally {
                
                boolean cancelled = playerSwingWorker.cancel(true);
                logger.log(Level.FINE, "Operation cancelled? " + cancelled);    
                playerSwingWorker = null;
                
            }
        }
        
        if (component != null) {
            component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        }

    }
    
    /**
     * Does not work...? using stop() and play() instead.
     */
    public static void skip() {
        if (playerSwingWorker != null && radioService != null) {
            try {
                logger.log(Level.INFO, "Skipping song.");
                radioService.executeCommand(Command.SKIP);
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ServiceException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    /**
     * This SwingWorker is to the the following:
     * 1) wait for the song to be almost finished
     * 2) check new metadata in short intervals
     * 3) if new metadata found then schedule new updater...
     */
    private static void scheduleMetadataUpdater(final int delay) {
        if (updaterSwingWorker == null) {
            
            updaterSwingWorker = new SwingWorker() {

                @Override
                protected Object doInBackground() throws Exception {
                    try {
                        // Let's make the duration shorter by 10%
                        Thread.sleep( (long) ((delay - (delay * 0.1)) * 1000));
                        // repeat refresh only 100 times ... then give up.
                        for (int i = 0; i < 100; i++) {
                            MetaData md = getRadio().getMetaData();
                            if (!md.toString().equals(metaData.toString())) {
                                updateMetadata(md);
                                return null;
                            }
                            Thread.sleep(REFRESH_INTERVAL * 1000);
                        }
                    } catch (InterruptedException e) {
                        // Exceptions.printStackTrace(e);
                        // song was stopped
                    }
                    return null;
                }
                
                @Override
                protected void done() {
                    if (updaterSwingWorker != null) {
                        logger.log(Level.FINEST, "is done? " + updaterSwingWorker.isDone());
                        logger.log(Level.FINEST, "is cancelled? " + updaterSwingWorker.isCancelled());
                        updaterSwingWorker = null;
                    }
                    scheduleMetadataUpdater(getTrackDuration(metaData));
                }
                
            };
            logger.log(Level.INFO, "scheduler updater will start approximately in " + delay + " seconds");
            updaterSwingWorker.execute();
        } else {
            logger.log(Level.INFO, "scheduler was not set!");
        }
    }
    
    private static Integer getTrackDuration(MetaData md) {
        Integer interval = DEFAUL_INTERVAL; // 60 sec
        if (md != null) {
            String duration = md.getTrack().getTrackDuration(); // why is this String?! Huh!
            try {
                interval = Integer.parseInt(duration);
            } catch (Exception e) {
                // ignore for now... use default value.
            }
        }
        return interval;
    }
    
    private static void updateMetadata(MetaData md) {
        
        if (dynamicContent != null) {
            
            // Did we receive metadata for a new track?
            // MetaData class does not override equals method
            //(I should add an issue for ascrblr into issues list)
            if (metaData != null && md != null &&
                    metaData.toString().equals(md.toString())) {
                // do nothing...
            } else {            
                // Remove old instance of MetaData from lookup
                if (metaData != null) {
                    logger.log(Level.FINEST, "Removing old MetaData from lookup");
                    dynamicContent.remove(metaData);
                }
                if (md != null) {
                    metaData = md;
                    // ... and add a new one
                    logger.log(Level.FINEST, "Adding new MetaData into lookup");
                    dynamicContent.add(metaData);
                }
            }
        }
        //logger.log(Level.FINEST, "Song metaData = " + metaData);
    }
    
    /**
     * 
     * @return MetaData MetaData for actual track
     */     
    public static MetaData getActualMetaData() {
        MetaData metadata = null;
        if (getRadio() != null) {
            try {                        
                metadata = getRadio().getMetaData();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ServiceException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return metadata;
    }
    
    /**
     * If there are new track metadata available then
     * update its instance in lookup. This will be reflected
     * by all relevant GUI components.
     */
    public static void checkNewMetaData() {
        updateMetadata(getActualMetaData());
    }
    
    /**
     * Returns number of seconds since the moment the player has been started.
     * This does not generally return number of seconds from the beginning of
     * the track (with the exception for the very first track).
     * 
     * @return int number of seconds
     */
    public static int getActualPlayerPosition() {
        int result = 0;
        if (jlPlayer.getJlPlayer() != null) {
            result = jlPlayer.getJlPlayer().getPosition();
        }
        return result;
    }
    
    public static StationUrl getStationUrl() {
        return stationUrl;
    }

    public static void setStationUrl(StationUrl stationUrl) {
        PlayerService.stationUrl = stationUrl;
    }

    public static Lookup getStaticLookup() {
        return lookup;
    }
    
    public Lookup getLookup() {
        // implementation of this method is forced by implemented interface
        // but we are using the static method instead...
        return lookup;
    }
    
}
